package com.i360day.invoker.codes.encoder;

import com.i360day.invoker.properties.HttpInvokerProperties;
import com.i360day.invoker.support.RemoteInvocationResult;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;

import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.List;

public class SpringEncoder implements Encoder {
    private final Log logger = LogFactory.getLog(SpringEncoder.class);
    private final ObjectFactory<HttpMessageConverters> messageConverters;
    private final HttpInvokerProperties.HttpInvokerResponseProperties responseProperties;

    public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters, HttpInvokerProperties.HttpInvokerResponseProperties responseProperties) {
        this.messageConverters = messageConverters;
        this.responseProperties = responseProperties;
    }

    @Override
    public void writeRemoteInvocationResult(OutputStream outputStream, Type responseType, RemoteInvocationResult result) throws IOException {
        write(result.getValue(), responseType, responseProperties.getContentType(), new HttpInvokerHttpOutputMessage(outputStream, responseProperties.getContentType()));
    }

    /**
     * Write the given RemoteInvocationResult to the given HTTP response.
     *
     * @param response     current HTTP response
     * @param responseType current HTTP responseType
     * @param result       the RemoteInvocationResult object
     */
    public void writeRemoteInvocationResult(HttpServletResponse response, Type responseType, RemoteInvocationResult result) {
        try {
            if (result.getException() != null || responseType == null) {
                int httpStatusCode = response.getStatus() == HttpStatus.OK.value() ? HttpStatus.INTERNAL_SERVER_ERROR.value() : response.getStatus();
                writeMessage(response, httpStatusCode, result.getStackTrace());
            } else {
                response.setContentType(responseProperties.getContentType().toString());
                write(result.getValue() == null ? "" : result.getValue(), responseType, responseProperties.getContentType(), new HttpInvokerHttpOutputMessage(response, responseProperties.getContentType()));
            }
        } catch (IOException e) {
            writeMessage(response, HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());
        }
    }

    /**
     * response out message
     *
     * @param response
     * @param code
     * @param message
     */
    protected void writeMessage(HttpServletResponse response, int code, String message) {
        try {
            if (response.getStatus() != 200) {
                code = response.getStatus();
            }
//            response.sendError(code, message);
            response.setStatus(code);
            write(message, String.class, responseProperties.getContentType(), new HttpInvokerHttpOutputMessage(response, responseProperties.getContentType()));
        } catch (IOException ex) {
            logger.error("http invoker server out message error...{}", ex);
        }
    }

    /**
     * out message
     *
     * @param data
     * @param responseType
     * @param mediaType
     * @param httpOutputMessage
     * @throws IOException
     */
    protected void write(Object data, Type responseType, MediaType mediaType, HttpOutputMessage httpOutputMessage) throws IOException {
        List<HttpMessageConverter<?>> converters = messageConverters.getObject().getConverters();
        for (HttpMessageConverter<?> messageConverter : converters) {
            if (messageConverter instanceof GenericHttpMessageConverter) {
                GenericHttpMessageConverter<Object> genericMessageConverter = (GenericHttpMessageConverter<Object>) messageConverter;
                if (genericMessageConverter.canRead(responseType, null, mediaType)) {
                    if (logger.isDebugEnabled()) {
                        ResolvableType resolvableType = ResolvableType.forType(responseType);
                        logger.debug("Reading to [" + resolvableType + "]");
                    }
                    genericMessageConverter.write(data, responseType, mediaType, httpOutputMessage);
                    return;
                }
            }
            if (responseType != null) {
                if (messageConverter.canRead(responseType.getClass(), mediaType)) {
                    if (logger.isDebugEnabled()) {
                        logger.debug("Reading to [" + responseType.getTypeName() + "] as \"" + mediaType + "\"");
                    }
                    ((HttpMessageConverter<Object>) messageConverter).write(data, mediaType, httpOutputMessage);
                    return;
                }
            }
        }
    }

    public class HttpInvokerHttpOutputMessage implements HttpOutputMessage {

        private HttpServletResponse response;
        private MediaType mediaType;
        private OutputStream outputStream;

        public HttpInvokerHttpOutputMessage(HttpServletResponse response, MediaType mediaType) throws IOException {
            this.response = response;
            this.outputStream = response.getOutputStream();
            this.mediaType = mediaType;
        }

        public HttpInvokerHttpOutputMessage(OutputStream outputStream, MediaType mediaType) {
            this.outputStream = outputStream;
            this.mediaType = mediaType;
        }

        @Override
        public OutputStream getBody() throws IOException {
            return outputStream;
        }

        @Override
        public HttpHeaders getHeaders() {
            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(mediaType);
            return httpHeaders;
        }
    }
}
